﻿#region Using References

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Beaker.OpenCube.Dynamic;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Framework;

#endregion

namespace Beaker.OpenCube.Specs
{
	[ExcludeFromCodeCoverage]
	[TestClass]
	public class WeakDictionarySpecs
	{
		#region Add

		[TestMethod]
		public void AddThrowsArgumentExceptionOnDuplicateKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var someObject1 = new SomeObject { Id = 1 };
			var someObject2 = new SomeObject { Id = 2 };
			cache.Add( 1, someObject1 );

			// act
			Action act = () => cache.Add( 1, someObject2 );

			// assert
			act.ShouldThrow<ArgumentException>();

			GC.KeepAlive( someObject1 );
			GC.KeepAlive( someObject2 );
		}

		[TestMethod]
		public void AddThrowsArgumentNullExceptionOnNullReferenceAsValue()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();

			// act
			Action act = () => cache.Add( 1, null );

			// assert
			act.ShouldThrow<ArgumentNullException>();
		}

		[TestMethod]
		public void AddAdds()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();

			// act
			var someObject = new SomeObject { Id = 5 };
			cache.Add( 5, someObject );

			// assert
			cache[5].Should().Be( someObject );
			GC.KeepAlive( someObject );
		}

		[TestMethod]
		public void AddingDuplicateKeyThatWasAlreadyFlushedShouldNotThrow()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			Add( cache, 5 ); // this will be flushed

			// act
			Collect();
			var someObject = new SomeObject { Id = 5 };
			cache.Add( 5, someObject );

			// assert
			cache[5].Should().Be( someObject );
			GC.KeepAlive( someObject );
		}

		#endregion
		#region Remove

		[TestMethod]
		public void RemovingNotExistingKeyReturnsFalse()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();

			// act
			bool result = cache.Remove( 7563 );

			// assert
			result.Should().BeFalse();
		}

		[TestMethod]
		public void RemovingExistingKeyReturnsTrue()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var someObject = new SomeObject { Id = 5 };
			cache.Add( 5, someObject );

			// act
			bool result = cache.Remove( 5 );

			// assert
			result.Should().BeTrue();
			GC.KeepAlive( someObject );
		}

		[TestMethod]
		public void RemovingFlushedKeyReturnsFalse()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			Add( cache, 5 );

			// act
			Collect();
			bool restul = cache.Remove( 5 );

			// assert
			restul.Should().BeFalse();
		}

		#endregion
		#region TryGetValue

		[TestMethod]
		public void TryGetShouldReturnFalseForNonExistingKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();

			// act
			SomeObject some;
			bool result = cache.TryGetValue( 5, out some );

			// assert
			result.Should().BeFalse();
			some.Should().BeNull();
		}

		[TestMethod]
		public void TryGetShouldReturnTrueForExistingKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var actualObject = new SomeObject { Id = 5 };
			cache.Add( 5, actualObject );

			// act
			SomeObject some;
			bool result = cache.TryGetValue( 5, out some );

			// assert
			result.Should().BeTrue();
			some.Should().BeSameAs( actualObject );

			GC.KeepAlive( actualObject );
		}

		[TestMethod]
		public void TryGetShouldReturnFalseForFlushedKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			Add( cache, 5 );

			// act
			Collect();
			SomeObject some;
			bool result = cache.TryGetValue( 5, out some );

			// assert
			result.Should().BeFalse();
			some.Should().BeNull();
		}

		#endregion
		#region Indexer

		// for: indexer
		// when: searching for non existing key
		// then: should throw KeyNotFoundException
		[TestMethod]
		public void IndexerShouldThrowKeyNotFoundExceptionWhenSearchingNonExistingKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();

			// act
			Action act = () => { SomeObject a = cache[5]; };

			// assert
			act.ShouldThrow<KeyNotFoundException>();
		}

		// for: indexer
		// when: searching for existing key
		// then: should return value belonging with the key
		[TestMethod]
		public void IndexerShouldReturnObjectWhenAskingExistingKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var actualObject = new SomeObject { Id = 5 };
			cache.Add( 5, actualObject );

			// act
			SomeObject some = cache[5];

			// assert
			some.Should().BeSameAs( actualObject );
			GC.KeepAlive( actualObject );
		}

		// for: indexer
		// when: searching for flushed key
		// then: should throw KeyNotFoundException
		[TestMethod]
		public void IndexerShouldThrowKeyNotFoundExceptionWhenSearchingFlushedKey()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			Add( cache, 5 );

			// act
			Collect();
			Action act = () => { SomeObject some = cache[5]; };

			// assert
			act.ShouldThrow<KeyNotFoundException>();
		}

		#endregion
		#region GC

		private void Collect()
		{
			GC.Collect( 2, GCCollectionMode.Forced, true );
			GC.WaitForPendingFinalizers();
			GC.Collect( 2, GCCollectionMode.Forced, true );
			GC.WaitForPendingFinalizers();
		}

		[TestMethod]
		public void NoFlushWhenHardReferenceToValue()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var someObject = new SomeObject { Id = 5 };
			cache.Add( 5, someObject );

			// act
			Collect();

			// assert
			cache[5].Should().Be( someObject );
			GC.KeepAlive( someObject );
		}

		[TestMethod]
		public void FlushWhenNoHardReferenceToValue()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			Add( cache, 5 );

			// act
			Collect();

			// assert
			cache.ContainsKey( 5 ).Should().BeFalse();
		}

		[TestMethod]
		public void CountReflectsOnlyUnflushedItems()
		{
			// arrange
			var cache = new WeakDictionary<int, SomeObject>();
			var someObject1 = new SomeObject { Id = 1 };
			cache.Add( 1, someObject1 );
			Add( cache, 2 );

			// act
			Collect();

			// assert
			cache.Count.Should().Be( 1 );

			GC.KeepAlive( someObject1 );
		}

		// add in method, so debug build will not hold on to a implict reference
		private void Add( WeakDictionary<int, SomeObject> dict, int id )
		{
			dict.Add( id, new SomeObject { Id = 5 } );
		}

		#endregion

		private sealed class SomeObject
		{
			public int Id { get; set; }
		}
	}
}
